问题背景
在执行增删改的时候,如果发现数据页没缓存,那么必然会基于free链表找到一个空闲的缓存页,然后读取到缓存页里去,但是如果已经缓存了,那么下一次就必然会直接使用缓存页。
需要更新的数据页都会在Buffer Pool的缓存页里,供你在内存中直接执行增删改的操作。
接着去更新Buffer Pool的缓存页中的数据,此时一旦你更新了缓存页中的数据,那么缓存页里的数据和磁盘上的数据页里的数据,是不是就不一致了?
这个时候,我们就说缓存页是脏数据,脏页
MySQL如何刷脏
MySQL如何区分Buffer Pool里的缓存页是脏页呢?数据库在这里引入了另外一个跟free链表类似的flush链表,这个flush链表本质也是通过缓存页的描述数据块中的两个指针,让被修改过的缓存页的描述数据块,组成一个双向链表。
凡是被修改过的缓存页,都会把他的描述数据块加入到flush链表中去,flush的意思就是这些都是脏页,后续都是要flush刷新到磁盘上去的
Buffer Pool的淘汰策略
随着不停的把磁盘上的数据页加载到空闲的缓存页里去,free链表中的空闲缓存页会越来越少,因为只要你把一个数据页加载到一个空闲缓存页里去,free链表中就会减少一个空闲缓存页。
所以,当你不停的把磁盘上的数据页加载到空闲缓存页里去,free链表中不停的移除空闲缓存页,迟早有那么一瞬间,你会发现free链表中已经没有空闲缓存页了
当所有的缓存页都被塞了数据了,此时无法从磁盘上加载新的数据页到缓存页里去了,那么此时就需要淘汰掉一些缓存页
这个过程可以简单理解为:把一个缓存页里被修改过的数据,给他刷到磁盘上的数据页里去,然后这个缓存页就可以清空了,让他重新变成一个空闲的缓存页
那么选择哪一个缓存页呢?这里MySQL使用了LRU淘汰算法,LRU就是Least Recently Used,最近最少使用的意思,简单的说还需要维护一个双向链表,每次进行增删改查操作时,将更新的节点挪到这个链表的头结点,那么随着时间的推移,命中数据次数多的缓存页就在前半部分,命中次数最少的就在链表的尾部,我们只需要淘汰尾部的节点就可以了。
LRU可能带来的问题
首先会带来隐患的就是MySQL的预读机制,这个所谓预读机制,说的就是当你从磁盘上加载一个数据页的时候,他可能会连带着把这个数据页相邻的其他数据页,也加载到缓存里去!
举个例子,假设现在有两个空闲缓存页,然后在加载一个数据页的时候,连带着把他的一个相邻的数据页也加载到缓存里去了,正好每个数据页放入一个空闲缓存页!
但是接下来呢,实际上只有一个缓存页是被访问了,另外一个通过预读机制加载的缓存页,其实并没有人访问,此时这两个缓存页可都在LRU链表的前面,如下图。
我们可以看到,前两个缓存页都是刚加载进来的,但是此时第二个缓存页是通过预读机制捎带着加载进来的,他也放到了链表的前面,但是他实际没人访问他。除了第二个缓存页之外,第一个缓存页,以及尾巴上两个缓存页,都是一直有人访问的那种缓存页,只不过上图代表的是刚刚把头部两个缓存页加载进来的时候的一个LRU链表当时的情况。
这个时候,假如没有空闲缓存页了,那么此时要加载新的数据页了,是不是就要从LRU链表的尾部把所谓的“最近最少使用的一个缓存页”给拿出来,刷入磁盘,然后腾出来一个空闲缓存页了?
这个时候,就会出问题了,如果你把上图中LRU尾部的那个缓存页刷入磁盘然后清空,但他可是之前一直频繁被人访问的,只不过在这一个瞬间,被新加载进来的两个缓存页给占据了LRU链表前面的位置,尤其是第二个缓存页,居然还是通过预读机制加载进来的,根本就不会有人访问!
那么这个时候,你要是把LRU链表尾部的缓存页给刷入磁盘,这是绝对不合理的,最合理的反而是把上图中LRU链表的第二个通过预读机制加载进来的缓存页给刷入磁盘和清空,毕竟他几乎是没什么人会访问的!
什么情况下会触发MySQL的预读机制
现在我们已经理解了预读机制一下子把相邻的数据页加载进缓存,放入LRU链表前面的隐患了,预读机制加载进来的缓存页可能根本不会有人访问,结果他却放在了LRU链表的前面,此时可能会把LRU尾部的那些被频繁访问的缓存页刷入磁盘中!
我们来看看,到底哪些情况下会触发MySQL的预读机制呢?
-
innodb_read_ahead_threshold,他的默认值是56,意思就是如果顺序的访问了一个区里的多个数据页,访问的数据页的数量超过了这个阈值,此时就会触发预读机制,把下一个相邻区中的所有数据页都加载到缓存里去
-
如果Buffer Pool里缓存了一个区里的13个连续的数据页,而且这些数据页都是比较频繁会被访问的,此时就会直接触发预读机制,把这个区里的其他的数据页都加载到缓存里去,这个机制是通过参数innodb_random_read_ahead来控制的,他默认是OFF,也就是这个规则是关闭的
所以默认情况下,主要是第一个规则可能会触发预读机制,一下子把很多相邻区里的数据页加载到缓存里去,这些缓存页如果一下子都放在LRU链表的前面,导致本来就在缓存里的一些频繁被访问的缓存页在LRU链表的尾部
另外一种场景
接着我们讲另外一种可能导致频繁被访问的缓存页被淘汰的场景,那就是全表扫描
比如
SELECT * FROM USERS
此时他没加任何一个where条件,会导致他直接一下子把这个表里所有的数据页,都从磁盘加载到Buffer Pool里去。
这个时候他可能会一下子就把这个表的所有数据页都一一装入各个缓存页里去!此时可能LRU链表中排在前面的一大串缓存页,都是全表扫描加载进来的缓存页!那么如果这次全表扫描过后,后续几乎没用到这个表里的数据,而链表的尾部都是频繁访问的数据,然后当你要淘汰掉一些缓存页腾出空间的时候,就会把LRU链表尾部一直被频繁访问的缓存页给淘汰掉了,而留下了之前全表扫描加载进来的大量的不经常访问的缓存页!
小节
MySQL的Buffer Pool实际上提供了一套完整的刷脏机制,更新过的缓存页使用一个叫做flush的双向链表记录着,假设随着时间的推移,buffer pool的内存越来越少,这个时候就需要清理buffer pool的内存了,buffer pool的淘汰策略采用的是LRU算法,即最近最少使用,将内存中的数据写入磁盘就是缓存淘汰了。
但是LRU算法往往会引来一些问题,MySQL的预读机制会将临近的数据页也加载到缓存页中,导致LRU算法的淘汰顺序不是我们想要的,还有就是在全表扫描时候,会将整个表的数据放在缓存页中,这样操作也会导致LRU尾部都是我们经常访问的数据。
思考
1.MySQL为什么设置预读机制? 2.加载一个数据页到缓存里去的时候,为什么要把一些相邻的数据页也加载到缓存里去呢?有何种意义? 3.是为了应对什么样的一个场景?
回答: 1.简单理解可以说是为了提升性能 2.为了优化性能,MySQL才设计了预读机制,也就是说如果在一个区内,顺序读取了好多数据页了,比如数据页01~数据页56都被你依次顺序读取了,MySQL会判断,你可能接着会继续顺序读取后面的数据页。 3.buffer pool 中当前区的数据页的随机访问量到达一定数量时,代表后续很有可能要访问到该区中的其他数据页,为避免每访问一个不在 buffer pool 中的数据页,就要从磁盘读一次到 buffer pool 中,索性先全部加载进来,同理,当 buffer pool 中当前区的数据页的顺序访问量到达一定数量时,代表后续很可能就要访问到下一个相邻区的数据页,为避免同样的问题,也把相邻区的数据页都加载进来